import { BinaryToTextEncoding, createHash, createHmac } from 'crypto'
import { isFunction, isNative } from 'lodash-es'
// @ts-ignore
import mergeSourceMap from 'merge-source-map'
import { TransformHook } from 'rollup'
import { URLSearchParams } from 'url'

export const extensions = ['tsx', 'ts', 'jsx', 'mjs', 'js', 'node', 'json']
export const escape = (str: string) =>
  str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')

export const hashString = (
  source: string,
  encoding: BinaryToTextEncoding = 'hex',
  length?: number
) => {
  const hash = createHash('sha1').update(source).digest(encoding)
  return length ? hash.slice(0, length).padStart(length, '_') : hash
}

export const getHashedIdentifier = (source: string, prefix = '_', length = 8) => {
  const hashStr = createHash('sha1').update(source).digest('base64url').replace(/-/g, '$')
  return `${prefix}${hashStr.slice(0, length)}`
}

let i = 0
export const getUniqueIdentifier = (prefix = '_', length = 8) => {
  const hashStr = createHmac('sha1', String(Math.random()))
    .update(String(Date.now()))
    .digest('base64url')
    .replace(/-/g, '$')
  return `${prefix}${hashStr.slice(0, length)}${(i++).toString(16)}`
}

export const toArray = <T = any>(data: any): T[] => {
  if (Array.isArray(data)) return data
  if (data == null) return []
  if (typeof data !== 'object') return [data]
  if (Symbol.iterator in data || 'length' in data) return Array.from(data as any)
  return [data]
}

export const mergeTransformHook = (...transforms: TransformHook[]): TransformHook => {
  transforms = transforms.filter(isFunction)
  return async function (code, id) {
    const rawCode = code
    let map, ast, meta, moduleSideEffects, syntheticNamedExports
    for (const transform of transforms) {
      const result = await transform.call(this, code, id)
      if (result) {
        if (typeof result === 'string') {
          code = result
        } else {
          ;({ ast, meta, moduleSideEffects, syntheticNamedExports } = result)
          code = result.code ?? code
          map = mergeSourceMap(map, result.map)
          result.meta
        }
      }
    }

    if (code == null || code === rawCode) {
      ast = code = map = undefined!
    }

    return { code, map, ast, meta, moduleSideEffects, syntheticNamedExports }
  }
}

export const urlSuffix = (
  hash?: string | null,
  ...querys: (URLSearchParams | string | null | undefined)[]
) => {
  let suffix = ''
  if (hash) {
    suffix += hash.startsWith('#') ? hash : '#' + hash
  }
  const _query = querys
    .map((query) =>
      typeof query === 'string' ? query : query?.toString().replace(/=(?=\&|$)/, '')
    )
    .filter(Boolean)
    .join('&')
  if (_query) {
    suffix += '?' + _query
  }

  return suffix
}

export const removeExtension = (name: string, ext?: string) => {
  if (!ext) return name.replace(/\.[^\.\\\/:;\?\*]+$/, '')
  if (!ext.startsWith('.')) {
    ext = '.' + ext
  }
  if (!name.endsWith(ext)) {
    return name
  }
  return name.slice(0, name.length - ext.length)
}

export const addExtension = (name: string, ext: string) => {
  if (!ext) return name.replace(/\.[^\.\\\/:;\?\*]+$/, '')
  if (!ext.startsWith('.')) {
    ext = '.' + ext
  }
  if (name.endsWith(ext)) {
    return name
  }
  return name + ext
}

export const removeExtensions = (name: string, extensions: string[]) => {
  for (const ext of extensions) {
    if (name.endsWith(ext)) {
      return name.slice(0, name.length - ext.length)
    }
  }
  return name
}

export function countSlash(value: string) {
  return (value.match(/\//g) || []).length
}

const dynamicRouteRE = /^\[.+\]$/
export const nuxtDynamicRouteRE = /^_[\s\S]*$/

export function isDynamicRoute(routePath: string, nuxtStyle: Boolean = false) {
  return nuxtStyle ? nuxtDynamicRouteRE.test(routePath) : dynamicRouteRE.test(routePath)
}

export function isCatchAllRoute(routePath: string, nuxtStyle: Boolean = false) {
  return nuxtStyle ? /^_$/.test(routePath) : /^\[\.{3}/.test(routePath)
}

export type UnkownValue = typeof UnkownValue
export const UnkownValue = Symbol.for('UnkownValue')
export class AnyValueWrapper {
  constructor(public value: string) {}
}
const getUniqueString = () => {
  return (
    createHmac('sha256', String(Math.random())).update(String(Date.now())).digest('base64') + ++i
  )
}
export const createAnyValueStringify = (
  stringifyValue?: (this: any, key: string, value: any) => string | void
) => {
  const replacements = new Map<string, string>()

  const addPlaceholderAndReturn = (value: string) => {
    let uniStr = getUniqueString()
    replacements.set(`"${uniStr}"`, value)
    return uniStr
  }

  function replacer(this: any, key: string, value: any) {
    const generateVal = stringifyValue?.call(this, key, value)
    if (generateVal != null) {
      return addPlaceholderAndReturn(generateVal)
    } else if (value instanceof Date) {
      return addPlaceholderAndReturn(`new Date(${value.getTime()})`)
    } else if (value instanceof RegExp) {
      return addPlaceholderAndReturn(`/${value.source}/${value.flags}`)
    } else if (value instanceof AnyValueWrapper) {
      return addPlaceholderAndReturn(value.value)
    } else if (value instanceof Map) {
      return addPlaceholderAndReturn(`new Map(${JSON.stringify(Array.from(value), replacer)})`)
    } else if (value instanceof Set) {
      return addPlaceholderAndReturn(`new Set(${JSON.stringify(Array.from(value), replacer)})`)
    } else if (isFunction(value)) {
      if (isNative(value)) {
        const nativeFuncStr = nativeFuncMap.get(value)
        if (nativeFuncStr) {
          return addPlaceholderAndReturn(nativeFuncStr)
        }
      } else {
        return addPlaceholderAndReturn(Function.prototype.toString.call(value))
      }
    }
    return value
  }

  return (value: any) => {
    let str = Array.isArray(value)
      ? `[${value.map((value) => JSON.stringify(value, replacer)).join(',')}]`
      : JSON.stringify(value, replacer)
    if (str) {
      for (const [placeholder, value] of replacements) {
        str = str.replace(placeholder, value)
      }
    }
    return str
  }
}

const nativeFuncMap = new Map<Function, string>(
  Object.entries({
    Object,
    Array,
    String,
    Boolean,
    Function,
    Symbol,
    console,
    Reflect,
    Date,
    Number,
    Math,
    parseInt,
    parseFloat,
    Error,
    Set,
    Map,
    WeakMap,
    WeakSet,
    Buffer,
    Int8Array,
    Int16Array,
    Int32Array,
    Uint8Array,
    Uint8ClampedArray,
    Uint16Array,
    Uint32Array,
    BigInt64Array,
    BigUint64Array,
    Float32Array,
    Float64Array,
  }).reduce((array, [key, value]) => {
    if (typeof value === 'function') {
      array.push([value, key])
      if (value.prototype && value !== Symbol) {
        const _prototype = value.prototype
        Reflect.ownKeys(_prototype).forEach((key$1) => {
          try {
            if (typeof key$1 === 'string') {
              const func = (_prototype as any)[key$1]
              if (typeof func === 'function') {
                array.push([func, `${key}.prototype.${key$1}`])
              }
            }
          } catch {}
        })
      }
    }
    Reflect.ownKeys(value).forEach((key$1) => {
      try {
        if (key$1 === 'prototype') return
        if (typeof key$1 === 'string') {
          const func = (value as any)[key$1]
          if (typeof func === 'function') {
            array.push([func, `${key}.${key$1}`])
          }
        }
      } catch {}
    })
    return array
  }, [] as [Function, string][])
)

export const uniqueString = (str: string, strings: Set<string>) => {
  if (strings.has(str)) {
    let i = 0
    let _str
    while (strings.has((_str = str + ++i)));
    str = _str
  }
  strings.add(str)
  return str
}
